/* ClearMerge, (C) 2011 Michael McTernan,
 *                      Michael.McTernan.2001@cs.bris.ac.uk
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

/**************************************************************************
 * Includes
 **************************************************************************/
#define WINVER 0x0600

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <stdbool.h>
#include <process.h>
#include <windows.h>
#include <tchar.h>
#include <winnt.h>
#include "amlicence.h"

/**************************************************************************
 * Data
 **************************************************************************/

/** Large buffer for command lines.
 * 32k is the longest allowable command line for CreateProcess().
 */
static char cmdBuffer[32768];

/**************************************************************************
 * Local Functions
 **************************************************************************/

/** Log a printf() style string to the log file.
 */
static void logString(const char *s, ...)
{
   const char *logFile = getenv("CLEARMERGE_LOG");

   if(logFile != NULL)
   {
      FILE *f = fopen(logFile, "a");

      if(f)
      {
         va_list va;

         va_start(va, s);
         vfprintf(f, s, va);
         va_end(va);

         fclose(f);
      }
   }
}


/** Produce a message box informing the user of a problem.
 *
 * \param[in] message   The message to display.
 */
static void failMessage(const TCHAR *message)
{
    MessageBox(NULL, message, TEXT("ClearMerge"), MB_OK | MB_ICONSTOP);
}


/** Read a line of text from some input file.
 *
 * \param[in]     f       Input file.
 * \param[in,out] buf     Buffer into which data should be read.
 * \param[in]     bufLen  Length of the buffer.
 * \return TRUE if a line was returned, FALSE if the EOF was reached.
 */
static bool readLine(FILE *f, char *buf, uint16_t bufLen)
{
    uint16_t t = 0;
    char     c;

    do
    {
        buf[t++] = c = fgetc(f);
    }
    while(t < bufLen && c != EOF && c != '\n' && c != '\r' && c != '\f');

    buf[t] = '\0';

    return !(t == 1 && c == EOF);
}


/** Check if some line is of the ClearCase map file should be patched.
 *
 * \param[in] buf        Buffer containing the input string to check.
 * \param[in] lineType   Map line type to check, either "xmerge" or "xcompare".
 * \returns  TRUE if the input line should be patched for installation/uninstall.
 */
static bool isInstallPatchLine(const char *buf, const char *lineType)
{
    const char *lineKey = "text_file_delta";
    const uint16_t lineKeyLen = strlen(lineKey);

    if(strncasecmp(buf, lineKey, lineKeyLen) == 0)
    {
        uint16_t t = lineKeyLen;

        while(isspace(buf[t]) && buf[t] != '\0')
        {
            t++;
        }
        return strncasecmp(&buf[t], lineType, strlen(lineType)) == 0;
    }

    return false;
}


/** Update the clearcase type map file.
 *
 * \param[in] mapfile     Path and filename of the ClearCase map file to update.
 * \param[in] installPath Path to ClearMerge application, or NULL for uninstall.
 * \param[in] merge       If true, update merge lines.
 * \param[in] diff        If true, update diff lines.
 * \returns TRUE if the installation was succesful.
 */
static bool installToClearCaseMap(const char *mapfile, const char *installPath, bool merge, bool diff)
{
    char outFile[MAX_PATH], buf[4096];
    FILE *in, *out;

    in = fopen(mapfile, "r");
    if(in == NULL)
    {
        logString("Failed to open ClearCase configuration map file '%s'.", mapfile);
        failMessage(TEXT("Failed to open ClearCase configuration map file."));
        return false;
    }

    snprintf(outFile, sizeof(buf), "%s.new", mapfile);
    out = fopen(outFile, "w");
    if(out == NULL)
    {
         logString("Failed to open new output configuration map file '%s'.", outFile);
        failMessage(TEXT("Failed to open new output file."));
        fclose(in);
        return false;
    }

    while(readLine(in, buf, sizeof(buf)))
    {
        uint16_t bufLen = strlen(buf);
        char lineEnd = buf[bufLen == 0 ? 0 : bufLen - 1];

        if(isInstallPatchLine(buf, "xmerge") && merge)
        {
            if(installPath != NULL)
            {
                fprintf(out, "text_file_delta     xmerge            %s%c", installPath, lineEnd);
            }
            else
            {
                fprintf(out, "text_file_delta     xmerge              ..\\..\\bin\\clearmrg.exe%c", lineEnd);
            }
        }
        else if(isInstallPatchLine(buf, "xcompare") && diff)
        {
            if(installPath != NULL)
            {
                fprintf(out, "text_file_delta     xcompare            %s%c", installPath, lineEnd);
            }
            else
            {
                fprintf(out, "text_file_delta     xcompare            ..\\..\\bin\\cleardiffmrg.exe%c", lineEnd);
            }
        }
        else
        {
            fprintf(out, "%s", buf);
        }
    }

    fclose(in);
    fclose(out);

    if(unlink(mapfile) != 0 || rename(outFile, mapfile) != 0)
    {
        failMessage(TEXT("Failed to update ClearCase configuration map file."));
        return FALSE;
    }

    return TRUE;
}


/** Read a value from the registry.
 * \param[in] root   Registry root handle.
 * \param[in] path   Path to the registry value to be read.
 */
static char *getRegKey(HKEY root, const TCHAR *path, const TCHAR *value)
{
    TCHAR val[MAX_PATH] = "";
    DWORD valSize = sizeof(val);
    char *r = NULL;
    HKEY  handle;


    /* Try to open the required key.
     *  Araxis comes in 64-bit and 32-bit versions, so try each registry view.
     */
    if(RegOpenKeyEx(root, path, 0, KEY_READ, &handle) != ERROR_SUCCESS &&
       RegOpenKeyEx(root, path, 0, KEY_READ | KEY_WOW64_64KEY, &handle) != ERROR_SUCCESS)
    {
        logString("Failed to find registry path '%s': code %u\n", path, GetLastError());
        return NULL;
    }

    /* Get the registry value */
    if(RegQueryValueEx(handle, value, NULL, NULL, val, &valSize) != ERROR_SUCCESS)
    {
        logString("Failed to get value of the key: code %u\n", GetLastError());
        return NULL;
    }
    else
    {
        r = strdup(val);
    }

    RegCloseKey(handle);

    return r;
}


/** Get the path to the ClearCase program.
 * This queries the Windows registry to find the install location of
 * cleartool.exe.
 *
 * On error this function produces a dialog box reporting an error and exits.
 *
 * \returns A pointer to an internal static buffer containing the path and
 *           filename.
 */
static char *getClearCaseMapPath(void)
{
    char *val = getRegKey(HKEY_LOCAL_MACHINE, TEXT("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\cleartool.exe"), NULL);
    int   t;

    if(val == NULL)
    {
        failMessage(TEXT("Could not locate ClearCase cleartool.exe."));
        exit(EXIT_FAILURE);
    }

    /* Remove the filename and last directory in the path */
    for(int p = 0; p < 2; p++)
    {
        t = strlen(val);
        while(t > 0 && val[t] != '\\')
        {
            t--;
        }

        val[t] = '\0';
    }

    /* Check if a trailing slash was found */
    if(t > 0)
    {
        const char tail = val[0] == '\"' ? '\"' : '\0';

        val = realloc(val, t + 16);
        if(val)
        {
            snprintf(&val[t], t + 16, "\\lib\\mgrs\\map%c", tail);
        }

        return val;
    }
    else
    {
        logString("Key value did not contain any backslashes; does not look like a path: code %u\n", GetLastError());
        failMessage(TEXT("Could not locate ClearCase installation."));
        exit(EXIT_FAILURE);
    }
}


/** Get the path to the Araxis ConsoleCompare program.
 * This queries the Windows registry to find the install location of
 * Merge.exe which is installed to the same directory as the
 * ConsoleCompare utility.
 *
 * On error this function produces a dialog box reporting an error and exits.
 *
 * \returns A pointer to an internal static buffer containing the path and
 *           filename.
 */
static char *getConsoleComparePath(void)
{
    TCHAR *val;

    /* Try v7.0 version first, then the v6.5 */
    val = getRegKey(HKEY_CLASSES_ROOT, TEXT("CLSID\\{5b30ff06-4854-418b-a388-334190776310}\\LocalServer32"), NULL);
    if(val == NULL)
    {
        val = getRegKey(HKEY_CLASSES_ROOT, TEXT("CLSID\\{6bc05a94-8ec8-11d2-b346-0000e835aa2c}\\LocalServer32"), NULL);
    }

    if(val != NULL)
    {
        /* Find the start of the last name of the path */
        int t = strlen(val);
        while(t > 0 && val[t] != '\\')
        {
            t--;
        }

        /* Check if a trailing slash was found */
        if(t > 0)
        {
            const char tail = val[0] == '\"' ? '\"' : '\0';

            val = realloc(val, t + 24);
            if(val)
            {
                snprintf(&val[t], t + 24, "\\consolecompare.exe%c", tail);
                return val;
            }
        }
        else
        {
            logString("Key value did not contain any backslashes; does not look like a path\n");
        }
    }

    failMessage(TEXT("Could not locate Araxis Merge installation."));
    exit(EXIT_FAILURE);
}


/** Get the Araxis Merge license key.
 */
static char *getAraxisMergeKey(void)
{
    char *val = getRegKey(HKEY_CURRENT_USER, TEXT("Software\\Araxis\\Merge\\7.0"), TEXT("SerialNumber"));

    if(!val)
    {
        val = getRegKey(HKEY_CURRENT_USER, TEXT("Software\\Araxis\\Merge\\6.5"), TEXT("SerialNumber"));
    }

    if(!val)
    {
        val = getRegKey(HKEY_LOCAL_MACHINE, TEXT("Software\\Araxis\\Merge\\7.0"), TEXT("SerialNumber"));
    }

    if(!val)
    {
        val = getRegKey(HKEY_LOCAL_MACHINE, TEXT("Software\\Araxis\\Merge\\6.5"), TEXT("SerialNumber"));
    }

    return val;
}


/** Run the command and wait for it to complete.
 *
 * \param[in] cmd  The command string to run.
 * \returns        Process exit code.
 */
static int run(char *cmd)
{
    PROCESS_INFORMATION pi;
    STARTUPINFO         si;

    memset(&si, 0, sizeof(si));
    si.cb = sizeof(si);

    logString("Command to execute:\n  %s", cmd);

    if(!CreateProcess(NULL, cmd, NULL, NULL, 0, 0, NULL, NULL, &si, &pi))
    {
        logString("CreateProcess() failed: code %u\n", GetLastError());
        failMessage("Failed to start Araxis.");
        exit(EXIT_FAILURE);
    }
    else
    {
        DWORD r;

        WaitForSingleObject(pi.hProcess, INFINITE);
        GetExitCodeProcess(pi.hProcess, &r);

        return r;
    }
}


/** Start a merge of 2 files.
 * This occurs in the case that a file is hijacked, updated and then checked
 * out.  At the checkout, ClearCase asks for a 2 way merge to get the updates
 * which won't have been applied due to the hijack.
 * We get something like the following arguments:
 * 0: xmerge
 * 1: -out
 * 2: C:\Views\MMcTernan_SUE\lte_fw_app_l1\code\l1_cphy.c.merge
 * 3: C:\Views\MMcTernan_SUE\lte_fw_app_l1\code\l1_cphy.c.keep
 * 4: C:\Views\MMcTernan_SUE\lte_fw_app_l1\code\l1_cphy.c
 */
static int xmergeUpdate(char **argv)
{
   logString("xmerge update:\n  %s\n  %s\n  into %s\n",
             argv[3], argv[4], argv[2]);

   snprintf(cmdBuffer,
            sizeof(cmdBuffer),
            "%s /wait /2 \"%s\" \"%s\" \"%s\"",
            getConsoleComparePath(),
            argv[3],
            argv[4],
            argv[2]);

   return run(cmdBuffer);
}


/** Start a merge.
 * We get something like the following arguments:
 *  0: xmerge
 * [1: -qall | -abort]
 *  1: -base
 *  2: \\server\vobs\file
 *  3: -out
 *  4: file.merge
 *  5: -fname
 *  6: file@@vob\ver
 *  7: -fname
 *  8: file@@vob\ver
 *  9: -fname
 * 10: file
 * 11: \\srv\hash
 * 12: file
 */
static int xmergeStatic(char **argv)
{
   /* If the -qall or -abort option is given, skip it */
   if(strcmp(argv[1], "-qall") == 0 || strcmp(argv[1], "-abort") == 0)
   {
      argv = &argv[1];
   }

   logString("xmerge static:\n  %s\t(as %s)\n  %s\t(as %s)\n  %s\t(as %s)\n  into %s\n",
             argv[2], argv[6], argv[11], argv[8], argv[12], argv[10], argv[4]);

   snprintf(cmdBuffer,
            sizeof(cmdBuffer),
            "%s /wait /3 /a1 /title1:\"%s\" /title2:\"%s\" /title3:\"%s\" \"%s\" \"%s\" \"%s\" \"%s\"",
            getConsoleComparePath(),
            argv[6],
            argv[8],
            argv[10],
            argv[2],
            argv[11],
            argv[12],
            argv[4]);

   return run(cmdBuffer);
}


/** Start a merge.
 * We get something like the following arguments:
 *  0: xmerge
 *  1: -base
 *  2: file@@vob\ver
 *  3: -out
 *  4: file.merge
 *  5: file@@vob\ver
 *  6: file
 */
static int xmergeDynamic(char **argv)
{
   logString("xmerge dynamic:\n  %s\n  %s\n  %s\n  into %s\n",
             argv[2], argv[5], argv[6], argv[4]);

   snprintf(cmdBuffer,
            sizeof(cmdBuffer),
            "%s /wait /3 /a1 /title1:\"%s\" /title2:\"%s\" /title3:\"%s\" \"%s\" \"%s\" \"%s\" \"%s\"",
            getConsoleComparePath(),
            argv[2],
            argv[5],
            argv[6],
            argv[2],
            argv[5],
            argv[6],
            argv[4]);

   return run(cmdBuffer);
}


/** Compare two files in a static view:
 * We get something like the following arguments:
 *  0: xcompare
 *  1: -fname
 *  2: file@@vob\ver
 *  3: -fname
 *  4: file
 *  5: \\srv\hash
 *  6: file
 */
static int xcompareStatic(char **argv)
{
   logString("xcompare static:\n  %s\t(as %s)\n  %s\t(as %s)\n",
             argv[5], argv[2], argv[6], argv[4]);

   snprintf(cmdBuffer,
            sizeof(cmdBuffer),
            "%s /nowait /2 /title1:\"%s\" /title2:\"%s\" \"%s\" \"%s\"",
            getConsoleComparePath(),
            argv[2],
            argv[4],
            argv[5],
            argv[6]);

   return run(cmdBuffer);
}


/** Compare two files in a dynamic view:
 * We get something like the following arguments:
 *  0: xcompare
 *  1: file@@vob\ver
 *  2: file
 */
static int xcompareDynamic(char **argv)
{
   logString("xcompare dynamic:\n  %s\n  %s\n",
             argv[1], argv[2]);

   snprintf(cmdBuffer,
            sizeof(cmdBuffer),
            "%s /nowait /2 /title1:\"%s\" /title2:\"%s\" \"%s\" \"%s\"",
            getConsoleComparePath(),
            argv[1],
            argv[2],
            argv[1],
            argv[2]);

   return run(cmdBuffer);
}


static bool checkInstall(bool calledForMerge)
{
    const amlicence_t lic = AmLicenceCheck(getAraxisMergeKey());

    if(lic == AMLICENCE_STD && calledForMerge)
    {
        switch(MessageBox(NULL,
                          TEXT("ClearCase wishes to perform a 3-way merge operation but Araxis Merge is only licensed for diffs."
                               "Would you like to uninstall ClearMerge from ClearCase for merge handling?"),
                          TEXT("ClearMerge"), MB_YESNOCANCEL | MB_DEFBUTTON1 | MB_ICONQUESTION))
        {
           case IDYES:
               /* Uninstall merge handling only */
               if(installToClearCaseMap(getClearCaseMapPath(), NULL, true, false))
               {
                   MessageBox(NULL,
                              TEXT("Handling of merge operations under ClearCase has been reverted to the default ClearCase merge tool."),
                              TEXT("ClearMerge"), MB_OK | MB_DEFBUTTON1 | MB_ICONSTOP);
               }
               exit(EXIT_FAILURE);

           case IDNO:
               return true;

           case IDCANCEL:
               exit(EXIT_FAILURE);
        }
    }

    if(lic == AMLICENCE_NOT_FOUND)
    {
        switch(MessageBox(NULL,
                          TEXT("ClearCase wishes to perform a diff or merge operation but Araxis Merge not correctly installed."
                               "Would you like to uninstall ClearMerge from ClearCase"),
                          TEXT("ClearMerge"), MB_YESNOCANCEL | MB_DEFBUTTON1 | MB_ICONQUESTION))
        {
            case IDYES:
                /* Uninstall */
                if(installToClearCaseMap(getClearCaseMapPath(), NULL, true, true))
                {
                    MessageBox(NULL,
                               TEXT("Handling of merge and diff operations under ClearCase have been reverted to the default ClearCase tools."),
                               TEXT("ClearMerge"), MB_OK | MB_DEFBUTTON1 | MB_ICONSTOP);

                }
                exit(EXIT_FAILURE);

            case IDNO:
                return true;

            case IDCANCEL:
                exit(EXIT_FAILURE);
        }

    }

    return true;
}


static void licence(void)
{
    printf("This program is free software: you can redistribute it and/or modify\n"
           "it under the terms of the GNU General Public License as published by\n"
           "the Free Software Foundation, either version 3 of the License, or\n"
           "(at your option) any later version.\n"
           "\n"
           "This program is distributed in the hope that it will be useful,\n"
           "but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
           "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n"
           "GNU General Public License for more details.\n"
           "\n"
           "You should have received a copy of the GNU General Public License\n"
           "along with this program.  If not, see <http://www.gnu.org/licenses/>.\n");
}


static void usage(void)
{
    printf("Usage: ClearMerge <options>\n"
           "\n"
           "Options:\n"
           "    -l          Print program license and exit.\n"
           "    -install    Update ClearCase map file to use this program for diffs and if\n"
           "                 Araxis Merge Pro or Pro Evaluation is installed, also install\n"
           "                 to handle merges.\n"
           "    -install-d  Install for ClearCase diffs.\n"
           "    -install-m  Install for ClearCase merges.\n"
           "    -uninstall  Revert ClearCase map file to default diff and compare\n"
           "                 configuration.\n"
           "    -am         Print path of Araxis ConsoleCompare.exe application.\n"
           "    -cc         Print path of the ClearCase application.\n"
           "    -aml        Check Araxis Merge licence.\n"
           "\n"
           "ClearMerge is a wrapper for Araxis Merge to provide ClearCase integration.\n"
           "This executable should be saved somewhere on your system, probably under\n"
           "C:\\Program Files\\Clearer Araxis and your ClearCase map file updated to run\n"
           "this program for the xmerge and xdiff targets.\n"
           "\n"
           "Environment Variables:\n"
           "\n"
           "  CLEARMERGE_LOG  If set, write debug information to the file named in this\n"
           "                   variable.\n"
           "\n"
           "\n"
           "ClearMerge comes with ABSOLUTELY NO WARRANTY.  This is free software, and\n"
           "you are welcome to redistribute it under certain conditions;\n"
           "type `ClearMerge -l' for details.\n"
           "\n"
           "ClearMerge " CM_VERSION " (C) 2010 Michael McTernan,\n"
           "                        Michael.McTernan.2001@cs.bris.ac.uk\n");
}

/**************************************************************************
 * Global Functions
 **************************************************************************/

int main(int argc, char **argv)
{
    logString("\nStarted with following arguments:\n");

    for(int t = 0; t < argc; t++)
    {
        logString("%4d: %s\n", t, argv[t]);
    }

    /* See how we were called */
    if(argc == 5 && strcmp(argv[0], "xmerge") == 0 && checkInstall(false))
    {
        return xmergeUpdate(argv);
    }
    if((argc == 13 || argc == 14) && strcmp(argv[0], "xmerge") == 0 && checkInstall(true))
    {
        return xmergeStatic(argv);
    }
    else if(argc == 7 && strcmp(argv[0], "xmerge") == 0 && checkInstall(true))
    {
        return xmergeDynamic(argv);
    }
    else if(argc == 7 && strcmp(argv[0], "xcompare") == 0 && checkInstall(false))
    {
        return xcompareStatic(argv);
    }
    else if(argc == 3 && strcmp(argv[0], "xcompare") == 0 && checkInstall(false))
    {
        return xcompareDynamic(argv);
    }
    else if(argc == 2 && strcmp(argv[1], "-l") == 0)
    {
        licence();
        return EXIT_SUCCESS;
    }
    else if(argc == 2 && strcmp(argv[1], "-am") == 0)
    {
        printf("%s\n", getConsoleComparePath());
        return EXIT_SUCCESS;
    }
    else if(argc == 2 && strcmp(argv[1], "-aml") == 0)
    {
        switch(AmLicenceCheck(getAraxisMergeKey()))
        {
            case AMLICENCE_STD:
                printf("Standard.\n");
                break;

            case AMLICENCE_PRO:
                printf("Professional.\n");
                break;

            case AMLICENCE_PRO_EVAL:
                printf("Professional evaluation.\n");
                break;

            case AMLICENCE_NOT_FOUND:
            default:
                printf("Licence not found or invalid.\n");
        }

        return EXIT_SUCCESS;
    }
    else if(argc == 2 && strcmp(argv[1], "-cc") == 0)
    {
        printf("%s\n", getClearCaseMapPath());
        return EXIT_SUCCESS;
    }
    else if(argc == 2 && strcmp(argv[1], "-uninstall") == 0)
    {
        return installToClearCaseMap(getClearCaseMapPath(), NULL, true, true) ? EXIT_SUCCESS : EXIT_FAILURE;
    }
    else if(argc == 2 && strcmp(argv[1], "-install") == 0)
    {
        char app[MAX_PATH];

        if(GetModuleFileName(NULL, app, sizeof(app)) != 0)
        {
            const amlicence_t lic = AmLicenceCheck(getAraxisMergeKey());
            bool              mrg = false;

            if(lic == AMLICENCE_PRO || lic == AMLICENCE_PRO_EVAL)
            {
                mrg = true;
            }

            return installToClearCaseMap(getClearCaseMapPath(), app, mrg, true) ? EXIT_SUCCESS : EXIT_FAILURE;
        }
        else
        {
            failMessage(TEXT("Could not determine application path."));
            return EXIT_FAILURE;
        }
    }
    else if(argc == 2 && strcmp(argv[1], "-install-d") == 0)
    {
        char app[MAX_PATH];

        if(GetModuleFileName(NULL, app, sizeof(app)) != 0)
        {
            return installToClearCaseMap(getClearCaseMapPath(), app, false, true) ? EXIT_SUCCESS : EXIT_FAILURE;
        }
        else
        {
            failMessage(TEXT("Could not determine application path."));
            return EXIT_FAILURE;
        }
    }
    else if(argc == 2 && strcmp(argv[1], "-install-m") == 0)
    {
        char app[MAX_PATH];

        if(GetModuleFileName(NULL, app, sizeof(app)) != 0)
        {
            return installToClearCaseMap(getClearCaseMapPath(), app, true, false) ? EXIT_SUCCESS : EXIT_FAILURE;
        }
        else
        {
            failMessage(TEXT("Could not determine application path."));
            return EXIT_FAILURE;
        }
    }
    else
    {
        usage();
    }

    return EXIT_FAILURE;
}

/* END OF FILE */

